﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows;
using System.Windows.Threading;
using System.Windows.Shapes;
using System.Windows.Media;
using System.ComponentModel;
using GraphosData;

namespace GraphosControls
{
   
    public enum ArrowType
    {
        None,
        Single,
        Double
    }
    public class VisualEdge : Shape, INotifyPropertyChanged
    {
        public VisualEdge(VisualNode start, VisualNode end)
        {
            DataContext = this;

            m_startNode = start;
            m_endNode = end;

            // set up bindings for this edge
            Binding startBinding = new Binding("Position");
            startBinding.Source = m_startNode;
            Binding endBinding = new Binding("Position");
            endBinding.Source = m_endNode;

            SetBinding(VisualEdge.StartPointProperty, startBinding);
            SetBinding(VisualEdge.EndPointProperty, endBinding);
            
        }

        #region Dependency Properties

        public Point StartPoint
        {
            get { return (Point)GetValue(StartPointProperty); }
            set { SetValue(StartPointProperty, value); }
        }
        public static readonly DependencyProperty StartPointProperty =
             DependencyProperty.Register("StartPoint", typeof(Point), typeof(VisualEdge),
             new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsMeasure));

        public Point EndPoint
        {
            get { return (Point)GetValue(EndPointProperty); }
            set { SetValue(EndPointProperty, value); }
        }
        public static readonly DependencyProperty EndPointProperty =
                 DependencyProperty.Register("EndPoint", typeof(Point), typeof(VisualEdge),
                 new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsMeasure));

        public Point Center
        {
            get { return (Point)GetValue(CenterProperty); }
            set { SetValue(CenterProperty, value); }
        }
        public static readonly DependencyProperty CenterProperty =
            DependencyProperty.Register("Center", typeof(Point), typeof(VisualEdge),
            new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.None));

        public VisualNode CenterNode
        {
            get { return (VisualNode)GetValue(CenterNodeProperty); }
            set { SetValue(CenterNodeProperty, value); }
        }
        public static readonly DependencyProperty CenterNodeProperty =
            DependencyProperty.Register("CenterNode", typeof(VisualNode), typeof(VisualEdge),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None));

        public VisualEdgeWeight Weight
        {
            get { return (VisualEdgeWeight)GetValue(WeightProperty); }
            set { SetValue(WeightProperty, value); }
        }
        public static readonly DependencyProperty WeightProperty =
             DependencyProperty.Register("Weight", typeof(VisualEdgeWeight), typeof(VisualEdge), 
             new FrameworkPropertyMetadata(VisualEdgeWeight.Normal, FrameworkPropertyMetadataOptions.None));

        public VisualEdgeType Type
        {
            get { return (VisualEdgeType)GetValue(TypeProperty); }
            set { SetValue(TypeProperty, value); }
        }
        public static readonly DependencyProperty TypeProperty =
            DependencyProperty.Register("Type", typeof(VisualEdgeType), typeof(VisualEdge), 
            new FrameworkPropertyMetadata(VisualEdgeType.Primary, FrameworkPropertyMetadataOptions.None));

        public ArrowType Arrow
        {
            get { return (ArrowType)GetValue(ArrowProperty); }
            set { SetValue(ArrowProperty, value); }
        }
        public static readonly DependencyProperty ArrowProperty =
             DependencyProperty.Register("Arrow", typeof(ArrowType), typeof(VisualEdge),
             new FrameworkPropertyMetadata(ArrowType.None, FrameworkPropertyMetadataOptions.None));

        public double HeadWidth
        {
            get { return (double)GetValue(HeadWidthProperty); }
            set { SetValue(HeadWidthProperty, value); }
        }
        public static readonly DependencyProperty HeadWidthProperty =
             DependencyProperty.Register("HeadWidth", typeof(double), typeof(VisualEdge),
             new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.None));

        public double HeadHeight
        {
            get { return (double)GetValue(HeadHeightProperty); }
            set { SetValue(HeadHeightProperty, value); }
        }
        public static readonly DependencyProperty HeadHeightProperty =
             DependencyProperty.Register("HeadHeight", typeof(double), typeof(VisualEdge),
             new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.None));

        #endregion

        #region Properties

        public VisualNode VisualNodeFrom
        {
            get { return m_startNode; }
        }

        public VisualNode VisualNodeTo
        {
            get { return m_endNode; }
        }

        public double Length
        {
            get { return (m_startNode.Position - m_endNode.Position).Length; }
        }

        public TextBlock LabelTextBlock
        {
            get 
            {
                return m_tbLabel; 
            }
        }

        public TextBlock NewLabelTextBlock
        {
            get
            {
                CreateNewLabel();
                return m_tbLabel;
            }
        }

        public double DesiredLabelOpacity
        {
            get
            {
                double opacity = 1.0;
                if (Type == VisualEdgeType.Secondary)
                    opacity = 0.0;
                return opacity;
            }
        }

        public double DesiredLabelFontSize
        {
            get
            {
                double size = 10;
                if (Type == VisualEdgeType.Secondary)
                    size = 10;
                return size;
            }
        }

        public Brush DesiredStrokeBrush
        {
            get
            {
                string name = "";
                if (Type == VisualEdgeType.Secondary)
                    name = "SecondaryEdgeBrush";
                else if (Weight == VisualEdgeWeight.Lesser)
                    name = "LesserEdgeBrush";
                else if (Weight == VisualEdgeWeight.Minor)
                    name = "MinorEdgeBrush";
                else
                    name = string.Format("{0}EdgeBrush", VisualNodeTo.Node.Person.Gender.ToString());

                return (Brush)TryFindResource(name);
            }
        }

        public Brush DesiredFillBrush
        {
            get
            {
                string name = string.Empty;
                if (Type == VisualEdgeType.Primary)
                    name = "PrimaryEdgeFillBrush";
                else
                    name = "SecondaryEdgeFillBrush";

                return (Brush)TryFindResource(name);
            }
        }

        public double DesiredStrokeThickness
        {
            get
            {
                double thick = 0.0;
                if (Type == VisualEdgeType.Secondary)
                    thick = 0.5;
                else
                {
                    switch (Weight)
                    {
                        case VisualEdgeWeight.Major:
                            thick = 1;
                            break;
                        case VisualEdgeWeight.Greater:
                            thick = 0.75;
                            break;
                        case VisualEdgeWeight.Normal:
                            thick = 0.5;
                            break;
                        case VisualEdgeWeight.Lesser:
                            thick = 0.25;
                            break;
                        default:
                            thick = 0.25;
                            break;
                    }
                }
                return thick;
            }
        }

        #endregion

        #region Overrides

        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);
            Style = (Style)TryFindResource("VisualEdgeStyle");
            Weight = BiblicalPeopleUtility.GetEdgeWeight(VisualNodeFrom.Node.RelationOf(VisualNodeTo.Node));
            CreateNewLabel();
            UpdateStyles();
        }

        #endregion

        #region Public Methods

        public void ResetProperties()
        {
            if (HasAnimatedProperties)
                TransitionUtility.ApplyEdgeResetAnimation(this);

           
        }

        public void UpdateStyles()
        {
            Type = VisualNodeFrom.IsRootNode || VisualNodeTo.IsRootNode ? VisualEdgeType.Primary : VisualEdgeType.Secondary;

            if (Stroke != null)
                TransitionUtility.ApplyEdgeResetAnimation(this);
            Stroke = DesiredStrokeBrush;
            Fill = DesiredFillBrush;
            StrokeThickness = DesiredStrokeThickness;
            LabelTextBlock.FontSize = DesiredLabelFontSize;
            LabelTextBlock.Opacity = DesiredLabelOpacity;
        }
        public void SwitchDirection()
        {
            VisualNode save = m_startNode;
            m_startNode = m_endNode;
            m_endNode = save;

            BindingOperations.ClearAllBindings(this);

            Binding startBinding = new Binding("Position");
            startBinding.Source = m_startNode;
            Binding endBinding = new Binding("Position");
            endBinding.Source = m_endNode;

            SetBinding(VisualEdge.StartPointProperty, startBinding);
            SetBinding(VisualEdge.EndPointProperty, endBinding);

            Weight = BiblicalPeopleUtility.GetEdgeWeight(VisualNodeFrom.Node.RelationOf(VisualNodeTo.Node));

            CreateNewLabel();
            UpdateStyles();
        } 

        #endregion

        #region Abstract Implementation

        protected override Geometry DefiningGeometry
        {
            get
            {
                // Create a StreamGeometry for describing the shape
                StreamGeometry geometry = new StreamGeometry();
                geometry.FillRule = FillRule.EvenOdd;

                using (StreamGeometryContext context = geometry.Open())
                {
                    InternalDrawArrowGeometry(context);
                }

                // Freeze the geometry for performance benefits
                geometry.Freeze();

                return geometry;
            }
        } 

        #endregion

        #region Private Helpers


        /// <summary>
        /// Returns an integer from 1 to 4 indicating which quadrant a line between two nodes intersects the edge of a node of given dimensions.
        /// </summary>
        /// <param name="nodePosition">The position of the node we are calulating the intersection on.</param>
        /// <param name="nodeDimensions">The dimensions of the node. The X coordinate is the width, and the Y coordinate is the height.</param>
        /// <param name="queryPosition">The endpoint of the line we are intersecting with the node edge.</param>
        /// <returns></returns>
        private static int Quandrant(Point nodePosition, Point nodeDimensions, Point queryPosition)
        {
            Double upSlope = nodeDimensions.Y / nodeDimensions.X;
            Double downSlope = -1.0 * upSlope;
            Double computedX = queryPosition.X - nodePosition.X;

            Double computedUpSliceY = upSlope * computedX + nodePosition.Y;
            Double computedDownSliceY = downSlope * computedX + nodePosition.Y;

            if (computedUpSliceY <= queryPosition.Y)
                if (computedDownSliceY <= queryPosition.Y)
                    return 4;
                else
                    return 3;
            else
                if (computedDownSliceY <= queryPosition.Y)
                    return 1;
                else
                    return 2;
        }

        /// <summary>
        /// Returns the point where a line between two nodes intersects the edge of the first node.
        /// </summary>
        /// <param name="from">The start of the line.</param>
        /// <param name="to">The end of the line.</param>
        /// <returns></returns>
        private static Point FindEdgePoint(VisualNode from, VisualNode to)
        {
            Vector up = new Vector(0, -1);
            Vector down = new Vector(0, 1);
            Vector right = new Vector(1, 0);
            Vector left = new Vector(-1, 0);
            Vector v = to.Position - from.Position;

            Point startPoint = from.Position;
            Double theta;

            switch (Quandrant(from.Position, from.Center, to.Position))
            {
                case 1:
                    startPoint.X = from.Position.X + from.Center.X;
                    theta = (Vector.AngleBetween(right, v) * Math.PI) / 180;
                    startPoint.Y = from.Position.Y + from.Center.X * Math.Tan(theta);
                    break;

                case 2:
                    theta = (Vector.AngleBetween(up, v) * Math.PI) / 180;
                    startPoint.X = from.Position.X + from.Center.Y * Math.Tan(theta);
                    startPoint.Y = from.Position.Y - from.Center.Y;
                    break;

                case 3:
                    startPoint.X = from.Position.X - from.Center.X;
                    theta = (Vector.AngleBetween(left, v) * Math.PI) / 180;
                    startPoint.Y = from.Position.Y - from.Center.X * Math.Tan(theta);
                    break;

                case 4:
                    theta = (Vector.AngleBetween(down, v) * Math.PI) / 180;
                    startPoint.X = from.Position.X - from.Center.Y * Math.Tan(theta);
                    startPoint.Y = from.Position.Y + from.Center.Y;
                    break;
            }

            return startPoint;
        }

        /// <summary>
        /// Draws an edge.
        /// </summary>
        /// <param name="context"></param>
        private void InternalDrawArrowGeometry(StreamGeometryContext context)
        {
            // If nodes have not been positioned, we can't do anything.
            if (m_startNode.Center.X == 0)
                return;

            // Find the end points of the edge.
            Point startPoint = FindEdgePoint(m_startNode, m_endNode);
            Point endPoint = FindEdgePoint(m_endNode, m_startNode);
            Point originalStart = startPoint;

            // Shrink the line.
            Vector v = endPoint - startPoint;
            v = v - (10 * (v / v.Length));
            startPoint = endPoint - v;
            endPoint = v + originalStart;

            if (v.Length < 30)
            {
                LabelTextBlock.Visibility = Visibility.Hidden;
            }
            else
            {
                LabelTextBlock.Visibility = Visibility.Visible;
            }

            if (CenterNode.Position == new Point())
            {
                CenterNode.Position = startPoint;
                return;
            }

            Point labelPosition = startPoint + 2 * (v / 3.0);

            Canvas.SetLeft(m_tbLabel, labelPosition.X);
            Canvas.SetTop(m_tbLabel, labelPosition.Y);

            // Start the edge.
            context.BeginFigure(endPoint, true, true);
            context.QuadraticBezierTo(CenterNode.Position, startPoint, true, false);

            Vector toControl = CenterNode.Position - startPoint;
            Vector projection = (Vector.Multiply(toControl, v) / v.LengthSquared) * v;

            projection.Normalize();

            projection = new Vector(projection.Y, -1.0 * projection.X);
            projection *= StrokeThickness * 6;

            context.QuadraticBezierTo(CenterNode.Position + projection, endPoint, true, true);          

            if (Arrow == ArrowType.Single || Arrow == ArrowType.Double)
            {
                Vector arrowDirection = (Vector)(CenterNode.Position - endPoint);
                arrowDirection.Normalize();

                Point arrowHead = endPoint - 3 * arrowDirection;

                Vector perp = new Vector(-1.0 * arrowDirection.Y, arrowDirection.X);

                perp *= 4;

                arrowDirection *= 3;

                Point arrowPoint1 = endPoint + perp + arrowDirection;
                Point arrowPoint2 = endPoint - perp + arrowDirection;

                context.LineTo(arrowPoint1, true, true);
                context.LineTo(arrowHead, true, true);
                context.LineTo(arrowPoint2, true, true);
            }
        }
        private void CreateNewLabel()
        {
            m_tbLabel = new TextBlock();
            m_tbLabel.Style = (Style)TryFindResource("LabelStyle");
            m_tbLabel.Text = m_startNode.Node.RelationOf(m_endNode.Node).ToString();
            InvalidateMeasure();
        }

        #endregion

        #region Fields

        private TextBlock m_tbLabel;
        private VisualNode m_startNode;
        private VisualNode m_endNode; 

        #endregion

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string info)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(info));
        }

        #endregion
    }
}
